Простые приложения на Go
Простые приложения на Go
Язык программирования Go (Golang) создан для написания надежных, эффективных и понятных системных утилит. Его стандартная библиотека предоставляет мощные инструменты для работы с файлами, сетью, параллелизмом и структурированными данными без необходимости подключения сторонних зависимостей. В этой главе рассматриваются практические примеры создания консольных приложений и серверов, демонстрирующие ключевые особенности синтаксиса и экосистемы языка.
Генератор паролей
Генератор паролей демонстрирует работу со строками, массивами символов и генерацией случайных чисел. Стандартная библиотека math/rand позволяет создавать криптографически стойкие последовательности через пакет crypto/rand.
Код примера
package main
import (
"crypto/rand"
"fmt"
"math/big"
)
func generatePassword(length int) string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*"
if length <= 0 {
return ""
}
password := make([]byte, length)
for i := range password {
// Получаем случайное число в диапазоне [0, len(charset))
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
if err != nil {
panic(err)
}
password[i] = charset[num.Int64()]
}
return string(password)
}
func main() {
pass := generatePassword(16)
fmt.Println("Сгенерированный пароль:", pass)
}
Разбор кода
- Пакет
crypto/rand: Используется вместоmath/randдля получения безопасных случайных чисел, что критично для генерации паролей. - Структура данных
[]byte: Слайс байтов используется как буфер для хранения символов будущего пароля. Это эффективно по памяти. - Цикл
for range: Итерация по слайсу автоматически определяет индекс и значение элемента. - Преобразование типов: Функция
num.Int64()преобразует большое целое число изbig.Intв стандартный типint64для использования в качестве индекса строки.
Сортировщик текстового файла
Этот пример показывает чтение текста из файла, разделение его на строки, сортировку списка строк и запись результата обратно в файл.
Код примера
package main
import (
"bufio"
"fmt"
"os"
"sort"
"strings"
)
func sortFile(inputPath, outputPath string) error {
file, err := os.Open(inputPath)
if err != nil {
return fmt.Errorf("ошибка открытия файла: %w", err)
}
defer file.Close()
var lines []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line != "" {
lines = append(lines, line)
}
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("ошибка чтения: %w", err)
}
sort.Strings(lines)
outFile, err := os.Create(outputPath)
if err != nil {
return fmt.Errorf("ошибка создания файла: %w", err)
}
defer outFile.Close()
writer := bufio.NewWriter(outFile)
for _, line := range lines {
if _, err := writer.WriteString(line + "\n"); err != nil {
return fmt.Errorf("ошибка записи: %w", err)
}
}
return writer.Flush()
}
func main() {
err := sortFile("input.txt", "output.txt")
if err != nil {
fmt.Println("Ошибка:", err)
os.Exit(1)
}
fmt.Println("Файл успешно отсортирован.")
}
Разбор кода
bufio.Scanner: Эффективный инструмент для посимвольного или построчного чтения больших файлов без загрузки всего содержимого в память.strings.TrimSpace: Удаляет пробелы и символы перевода строки по краям каждой строки перед обработкой.sort.Strings: Встроенная функция сортировки, которая использует алгоритм быстрой сортировки (quicksort) для упорядочивания слайса строк.defer: Обеспечивает закрытие файловых дескрипторов при выходе из функции, предотвращая утечки ресурсов.writer.Flush(): Явно сбрасывает буфер вывода на диск, гарантируя сохранение данных.
Консольный калькулятор
Калькулятор реализует базовую арифметику с обработкой ввода пользователя и разделением логики вычислений на отдельные функции.
Код примера
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func calculate(a float64, b float64, op string) (float64, error) {
switch op {
case "+":
return a + b, nil
case "-":
return a - b, nil
case "*":
return a * b, nil
case "/":
if b == 0 {
return 0, fmt.Errorf("деление на ноль невозможно")
}
return a / b, nil
default:
return 0, fmt.Errorf("неподдерживаемая операция: %s", op)
}
}
func main() {
reader := bufio.NewReader(os.Stdin)
fmt.Print("Введите первое число: ")
num1Str, _ := reader.ReadString('\n')
num1, _ := strconv.ParseFloat(strings.TrimSpace(num1Str), 64)
fmt.Print("Введите операцию (+, -, *, /): ")
opStr, _ := reader.ReadString('\n')
op := strings.TrimSpace(opStr)
fmt.Print("Введите второе число: ")
num2Str, _ := reader.ReadString('\n')
num2, _ := strconv.ParseFloat(strings.TrimSpace(num2Str), 64)
result, err := calculate(num1, num2, op)
if err != nil {
fmt.Println("Ошибка:", err)
return
}
fmt.Printf("Результат: %.2f\n", result)
}
Разбор кода
- Тип
float64: Используется для представления вещественных чисел с высокой точностью. switch: Конструкция выбора действия, которая четко разделяет логику для каждого оператора.- Обработка ошибок: Возврат ошибки при попытке деления на ноль или вводе некорректной операции.
strconv.ParseFloat: Преобразование строкового ввода в числовой тип с проверкой на корректность формата.
Трекер задач в JSON
Пример демонстрирует сериализацию (преобразование структур данных в JSON) и десериализацию, а также работу с файловой системой для сохранения состояния.
Структура данных
type Task struct {
ID int `json:"id"`
Title string `json:"title"`
Done bool `json:"done"`
Priority int `json:"priority"` // 1-высокий, 5-низкий
}
type TaskList struct {
Tasks []Task `json:"tasks"`
}
Код реализации
package main
import (
"encoding/json"
"fmt"
"os"
)
const dataFile = "tasks.json"
func loadTasks() (*TaskList, error) {
data, err := os.ReadFile(dataFile)
if err != nil {
if os.IsNotExist(err) {
return &TaskList{Tasks: []Task{}}, nil
}
return nil, err
}
var list TaskList
if err := json.Unmarshal(data, &list); err != nil {
return nil, err
}
return &list, nil
}
func saveTasks(list *TaskList) error {
data, err := json.MarshalIndent(list, "", " ")
if err != nil {
return err
}
return os.WriteFile(dataFile, data, 0644)
}
func addTask(list *TaskList, title string, priority int) {
newID := 1
if len(list.Tasks) > 0 {
newID = list.Tasks[len(list.Tasks)-1].ID + 1
}
list.Tasks = append(list.Tasks, Task{
ID: newID,
Title: title,
Done: false,
Priority: priority,
})
}
func main() {
list, err := loadTasks()
if err != nil {
fmt.Println("Ошибка загрузки:", err)
os.Exit(1)
}
addTask(list, "Изучить Go", 1)
addTask(list, "Написать статью", 2)
if err := saveTasks(list); err != nil {
fmt.Println("Ошибка сохранения:", err)
os.Exit(1)
}
fmt.Println("Задачи сохранены в tasks.json")
// Вывод для проверки
jsonOut, _ := json.MarshalIndent(list, "", " ")
fmt.Println(string(jsonOut))
}
Разбор кода
- Теги
json:"...": Позволяют управлять именами полей в JSON-структуре. Например, полеDoneсохраняется какdone. encoding/json: Стандартный пакет для работы с форматом JSON.json.MarshalIndent: Преобразует структуру в строку JSON с отступами для удобного чтения человеком.os.WriteFile: Современный аналог создания и записи файла с указанием прав доступа (0644).- Логика ID: Простая эвристика для генерации уникального идентификатора на основе последнего элемента списка.
Простой HTTP-сервер и клиент
Go обладает встроенным веб-сервером, который работает очень быстро благодаря встроенной поддержке многопоточности.
Серверная часть
package main
import (
"fmt"
"log"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
name = "Гость"
}
fmt.Fprintf(w, "Привет, %s! Добро пожаловать на сервер Go.", name)
}
func main() {
http.HandleFunc("/", handler)
fmt.Println("Сервер запущен на http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Клиентская часть
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
resp, err := http.Get("http://localhost:8080/?name=Разработчик")
if err != nil {
fmt.Println("Ошибка запроса:", err)
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("Ошибка чтения тела:", err)
return
}
fmt.Println("Ответ сервера:")
fmt.Println(string(body))
}
Разбор кода
http.HandleFunc: Регистрирует функцию обработки запросов для конкретного URL-пути.r.URL.Query().Get("name"): Извлекает параметры из строки запроса (например,?name=Value).http.Response: Объект ответа содержит статус код, заголовки и тело ответа.io.ReadAll: Чтение всего тела ответа в байтовый срез.defer resp.Body.Close(): Обязательная практика для освобождения сетевого ресурса после завершения работы.
Отправитель HTTP-запросов (с кастомными параметрами)
Этот пример расширяет функциональность клиента, позволяя отправлять POST-запросы с произвольными данными и заголовками.
Код примера
package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
func sendPostRequest(url string, payload map[string]string) error {
jsonData := []byte(`{"key": "value"}`) // Упрощенный пример JSON
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "Go-Custom-Client/1.0")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Printf("Статус: %d, Тело: %s\n", resp.StatusCode, string(body))
return nil
}
func main() {
sendPostRequest("http://example.com/api/data", nil)
}
Разбор кода
http.NewRequest: Создает объект запроса с указанным методом (GET, POST, PUT и т.д.) и телом.bytes.NewBuffer: Позволяет использовать байтовый срез как поток данных для тела запроса.req.Header.Set: Установка пользовательских заголовков, важных для идентификации клиента или формата данных.client.Do: Выполняет запрос и возвращает ответ, позволяя настроить таймауты и редиректы через структуруhttp.Client.
Утилита для сканирования директорий
Инструмент для обхода файловой системы и сбора информации о файлах и подпапках.
Код примера
package main
import (
"fmt"
"os"
"path/filepath"
)
func scanDirectory(root string) error {
return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
indent := ""
relPath, _ := filepath.Rel(root, path)
for i := 0; i < len(filepath.SplitList(relPath)); i++ {
indent += " "
}
typeStr := "файл"
if info.IsDir() {
typeStr = "каталог"
}
fmt.Printf("%s[%s] %s (%d байт)\n", indent, typeStr, info.Name(), info.Size())
return nil
})
}
func main() {
currentDir, _ := os.Getwd()
fmt.Println("Сканирование директории:", currentDir)
if err := scanDirectory(currentDir); err != nil {
fmt.Println("Ошибка:", err)
}
}
Разбор кода
filepath.Walk: Рекурсивно проходит по всей структуре директории, вызывая функцию для каждого найденного пути.os.FileInfo: Структура, содержащая метаданные файла (размер, права доступа, время изменения).info.IsDir(): Метод для определения типа объекта (файл или каталог).filepath.Rel: Вычисление относительного пути для красивого отображения структуры.
Скрипт для создания резервного копирования файлов
Автоматизация процесса дублирования файлов с добавлением временной метки к имени копии.
Код примера
package main
import (
"fmt"
"io"
"os"
"path/filepath"
"time"
)
func copyFile(src, dst string) error {
sourceFile, err := os.Open(src)
if err != nil {
return err
}
defer sourceFile.Close()
destFile, err := os.Create(dst)
if err != nil {
return err
}
defer destFile.Close()
if _, err := io.Copy(destFile, sourceFile); err != nil {
return err
}
return nil
}
func createBackup(sourceDir string) error {
timestamp := time.Now().Format("2006-01-02_15-04-05")
backupName := "backup_" + timestamp
backupPath := filepath.Join(sourceDir, backupName)
files, err := os.ReadDir(sourceDir)
if err != nil {
return err
}
for _, file := range files {
if file.IsDir() {
continue
}
srcPath := filepath.Join(sourceDir, file.Name())
dstPath := filepath.Join(backupPath, file.Name())
// Создание целевой директории бэкапа
os.MkdirAll(backupPath, 0755)
if err := copyFile(srcPath, dstPath); err != nil {
fmt.Printf("Не удалось скопировать %s: %v\n", file.Name(), err)
continue
}
fmt.Printf("Скопировано: %s\n", file.Name())
}
return nil
}
func main() {
dir := "."
fmt.Println("Начало резервного копирования...")
if err := createBackup(dir); err != nil {
fmt.Println("Ошибка:", err)
} else {
fmt.Println("Резервное копирование завершено.")
}
}
Разбор кода
time.Now().Format: Форматирование даты в строку, пригодную для имени файла (ISO 8601 стиль адаптирован под Windows/Linux).io.Copy: Оптимизированная функция для побайтового копирования данных между потоками.os.MkdirAll: Создание директории, если она не существует, включая все необходимые промежуточные уровни.filepath.Join: Корректное объединение путей с учетом разделителей для разных операционных систем.
Мониторинг дискового пространства
Утилита проверяет свободное место на диске и выводит статистику в процентах и абсолютных значениях.
Код примера
package main
import (
"fmt"
"os"
)
func checkDiskUsage(path string) error {
fs, err := os.Stat(path)
if err != nil {
return err
}
// Для Unix-систем используем syscall, но в чистом Go лучше использовать пакеты уровня выше
// Здесь пример с использованием статистики корневого каталога
// Примечание: Для точного объема диска часто требуется syscall или внешние библиотеки
// Но мы можем получить объем доступной памяти через os.Stat для тестов
// Альтернативный подход: получение статистики корня
rootStat, err := os.Stat("/")
if err != nil {
// Если нет прав на корень, пробуем текущую директорию
rootStat, err = os.Stat(".")
if err != nil {
return err
}
}
// В Go нет встроенной функции для получения общего объема диска в stdlib
// Поэтому этот пример демонстрирует концепцию проверки свободного места
// через анализ доступных операций или использование внешних инструментов.
// Однако, для демонстрации логики:
fmt.Printf("Путь: %s\n", path)
fmt.Printf("Статус: Доступен\n")
// Пример вывода (для реального проекта используйте syscall.Unix or golang.org/x/sys/unix)
// var stat syscall.Statfs_t
// syscall.Statfs(path, &stat)
// total := uint64(stat.Blocks) * uint64(stat.Bsize)
// free := uint64(stat.Bfree) * uint64(stat.Bsize)
fmt.Println("Метод: Анализ метаданных директории (демонстрация)")
return nil
}
func main() {
checkDiskUsage("/tmp") // Или текущая директория
}
Примечание: Стандартная библиотека Go не предоставляет прямой функции для получения общего объема диска и свободного места. Для этого обычно используют пакет syscall (Unix) или golang.org/x/sys/windows. Приведенный код демонстрирует структуру программы, готовую к интеграции системных вызовов.
Парсер URL и проверка доступности ресурса
Инструмент анализирует компоненты URL (хост, путь, порт) и проверяет, доступен ли сервер по этому адресу.
Код примера
package main
import (
"fmt"
"net/url"
"time"
)
func checkURL(target string) error {
parsedURL, err := url.Parse(target)
if err != nil {
return fmt.Errorf("неверный формат URL: %w", err)
}
fmt.Printf("Анализ URL: %s\n", target)
fmt.Printf(" Схема: %s\n", parsedURL.Scheme)
fmt.Printf(" Хост: %s\n", parsedURL.Host)
fmt.Printf(" Путь: %s\n", parsedURL.Path)
fmt.Printf(" Порт: %s\n", parsedURL.Port())
timeout := 5 * time.Second
client := &http.Client{
Timeout: timeout,
}
resp, err := client.Head(target)
if err != nil {
return fmt.Errorf("проверка недоступна: %w", err)
}
defer resp.Body.Close()
fmt.Printf(" Статус: %d\n", resp.StatusCode)
fmt.Printf(" Время ответа: OK\n")
return nil
}
func main() {
urls := []string{
"https://github.com",
"http://invalid-domain-xyz.com",
}
for _, u := range urls {
fmt.Println("---")
if err := checkURL(u); err != nil {
fmt.Println("Ошибка:", err)
}
}
}
Разбор кода
url.Parse: Разбивает строку URL на составные части (схема, хост, порт, путь).parsedURL.Port(): Извлекает номер порта, если он явно указан.client.Head: Отправка запроса метода HEAD, который получает только заголовки без тела ответа, что экономит трафик.Timeout: Ограничение времени ожидания ответа для предотвращения зависания программы при недоступном сервере.
Конвертер форматов дат
Утилита конвертирует строковые представления дат в другие форматы, используя встроенный механизм форматирования времени.
Код примера
package main
import (
"fmt"
"time"
)
func convertDate(dateString, layout, outputLayout string) error {
loc, err := time.LoadLocation("UTC")
if err != nil {
return err
}
t, err := time.ParseInLocation(layout, dateString, loc)
if err != nil {
return fmt.Errorf("неверный формат входной даты: %w", err)
}
formatted := t.Format(outputLayout)
fmt.Printf("Вход: %s (%s)\n", dateString, layout)
fmt.Printf("Выход: %s (%s)\n", formatted, outputLayout)
return nil
}
func main() {
input := "2026-05-06"
layout := "2006-01-02"
output := "02 January 2006"
convertDate(input, layout, output)
}
Разбор кода
time.ParseInLocation: Парсинг строки в объектtime.Timeс учетом временной зоны.t.Format: Преобразование объекта времени в строку по заданному шаблону.- Шаблон
2006-01-02: В Go используется специальное эталонное времяMon Jan 2 15:04:05 MST 2006для определения формата. Любая дата в коде должна соответствовать этому шаблону.
Утилита для просмотра запущенных процессов
Отображение списка активных процессов с их идентификаторами (PID) и командами запуска.
Код примера
package main
import (
"fmt"
"os/exec"
"strings"
)
func listProcesses() error {
var cmd *exec.Cmd
var args []string
// Определение команды в зависимости от ОС
if isWindows() {
cmd = exec.Command("tasklist", "/FO", "CSV")
args = []string{}
} else {
cmd = exec.Command("ps", "aux")
args = []string{}
}
output, err := cmd.Output()
if err != nil {
return fmt.Errorf("ошибка выполнения команды: %w", err)
}
lines := strings.Split(string(output), "\n")
for i, line := range lines {
if i == 0 && !isWindows() {
continue // Пропуск заголовка в Linux/Mac
}
if line == "" {
continue
}
fmt.Println(line)
}
return nil
}
func isWindows() bool {
return true // Заглушка для примера, в реальности используйте runtime.GOOS
}
func main() {
fmt.Println("Список процессов:")
if err := listProcesses(); err != nil {
fmt.Println("Ошибка:", err)
}
}
Примечание: Для реальной работы с процессами в Go рекомендуется использовать пакеты вроде github.com/shirou/gopsutil, так как прямое выполнение команд (exec.Command) зависит от ОС и требует наличия специфических утилит. Данный пример демонстрирует принцип взаимодействия с оболочкой.
Характерный пример именно для Go: Горизонтальная масштабируемость через Горутины
Главная особенность языка Go — это нативная поддержка параллелизма через горутины (goroutines) и каналы (channels). Ниже приведен пример, который обрабатывает список URL параллельно, используя ограниченное количество горуток.
package main
import (
"fmt"
"net/http"
"sync"
"time"
)
type Result struct {
URL string
Status int
Error error
}
func checkConcurrency(urls []string, workers int) []Result {
results := make([]Result, len(urls))
var wg sync.WaitGroup
semaphore := make(chan struct{}, workers)
for i, url := range urls {
wg.Add(1)
go func(index int, target string) {
defer wg.Done()
semaphore <- struct{}{} // Занимаем слот
defer func() { <-semaphore }() // Освобождаем слот
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Head(target)
status := 0
if err == nil {
status = resp.StatusCode
defer resp.Body.Close()
}
results[index] = Result{
URL: target,
Status: status,
Error: err,
}
}(i, url)
}
wg.Wait()
return results
}
func main() {
targets := []string{
"https://google.com",
"https://github.com",
"https://example.com",
"https://invalid-site-test.com",
}
fmt.Println("Запуск параллельной проверки...")
start := time.Now()
res := checkConcurrency(targets, 3) // Максимум 3 одновременных запроса
elapsed := time.Since(start)
for _, r := range res {
if r.Error != nil {
fmt.Printf("%s: Ошибка - %v\n", r.URL, r.Error)
} else {
fmt.Printf("%s: Статус %d\n", r.URL, r.Status)
}
}
fmt.Printf("Время выполнения: %v\n", elapsed)
}
Разбор кода
go func(...): Запуск функции в отдельной горуток. Горутина потребляет минимум памяти и переключается очень быстро.sync.WaitGroup: Синхронизатор, позволяющий основной программе ждать завершения всех запущенных горуток.chan struct{}(Семафор): Канал пустого типа, ограничивающий максимальное количество одновременно работающих горуток. Это защищает систему от перегрузки.defer: Гарантирует освобождение ресурса канала даже при возникновении ошибки внутри горутины.time.Since: Подсчет времени выполнения блока кода для оценки производительности.
Этот пример иллюстрирует философию Go: простота написания параллельного кода, безопасность потоков и высокая эффективность использования ресурсов процессора.